/*
 * UDP port handling code. Version 0.7a
 * Written by Nostradamus for Zebedee.
 * Developed from an original concept by Alvin@Sushi.
 */

#pragma strict_types
#include "/secure/config.h"
#include UDP_HEADER

/* --- Configurable definitions. --- */

/* CD muds will probably need these include file. */
/* #include <std.h> */
/* #include "/config/sys/local.h" */

/* Public commands which will be accessible to any unlisted muds.
 * PING, QUERY and REPLY are included by default. */
#define COMMANDS \
({ "channel", "finger", "ftp", "locate", "man", "tell", "who","mail","lista" \
, "webwho", "orla", "webwhoi" })

/* Define this to the object that receives incoming packets and passes
 * them to the inetd. Undefine for no receive_udp() security.
 * NOTE: The string must be of the format that object_name() returns. */
#define UDP_MASTER		__MASTER_OBJECT__
/* #define UDP_MASTER		SECURITY		// CD */
/* #define UDP_MASTER		MASTER */
/* #define UDP_MASTER		"obj/master" */

/* The function that is called at load-time if not create() or reset(). */
/* #define CREATE_FUN()		teleledningsanka()	// CD2 */
/* #define CREATE_FUN()		>>()			// CD3 */

/* How to set the euid for this object if using native mode.
 * Ensure that it can read the INETD_HOSTS file. */
/* #define SET_EUID		seteuid("root") */
#define SET_EUID		seteuid(getuid())

/* Define these as appropriate if you do not have the relevant efuns. */
/* #define to_int(x)		atoi(x)			// MudOS & CD */
/* #define to_string(x)		("" + x)		// CD */
/* #define copy(x)	copy(x)			// MudOS */
/* #define copy(x)	(x + ([ ]))		// CD */
/* #define call_out(fun, del, arg) \
		set_alarm(itof(del), 0.0, fun, arg)		// CD3 */
/* #define send_imp(host, port, data) \
		SECURITY->send_udp_message(host, port, data)	// CD */

/* Define if casts can only be used on unknown or mixed types. (CD) */
#undef RESTRICTED_CASTS

/* Define if your system does not support the a[n..] indexing notation. (CD) */
#undef USE_EXTRACT

/* Define if your system does not support closures. (CD / MudOS) */
#undef NO_CLOSURES

/* Define this if you are running another intermud package concurrently. */
/* #define RECEIVE_UDP_COMPAT(sender, packet) \
		UDP_MANAGER->incoming_udp(sender, packet)	// CD */

/* Define this if you are running another intermud package concurrently and
 * have a compatability module for it. */
/* #define SEND_UDP_COMPAT(mudname, data, expect_reply) \
		"/secure/udp_compat"->send_udp(mudname, data, expect_reply) */

/* The maximum number of characters we can send in one packet.
 * You may need to reduce this, but 512 should be safe. */
#define MAX_PACKET_LEN	1024

/* You shouldn't need to change anything below. */

#define USE_OLD_DELIMITER
#define DELIMITER_COMPAT
#define USE_OLD_DATA_FORMAT

#ifdef ZEBEDEE
#include <defs.h>
#endif

#ifndef DATE
#define DATE		ctime(time())[4..15]
#endif

/* --- End of Config. Do not alter anything below. --- */

#define UNKNOWN		0
#define UP		time()
#define DOWN		(-time())

#define NEW_DELIMITER	"\n"
#ifdef USE_OLD_DELIMITER
#define DELIMITER	"|"
#else
#define DELIMITER	"\n"
#endif
#define OLD_DELIMITER	"|"
#define HOSTFILE_DEL	":"
#define HOSTFILE_DEL2	","
#define RETRY		"_RETRY"

private mapping hosts, pending_data, incoming_packets;
private string *received_ids;
private int packet_id;

void set_host_list();
varargs string send_udp(string mudname, mapping data, int expect_reply);

#define DEBUG(msg)	if (find_object("pepel"))\
			tell_object(find_object("pepel"), msg);

void my_create() {
#ifndef COMPAT_FLAG
    SET_EUID;
#endif
    packet_id = 0;
    pending_data = ([ ]);
    incoming_packets = ([ ]);
    hosts = ([ ]);
    received_ids = ({ });
    set_host_list();
    if (!this_player())
	call_out("startup", 1, 0);
}

#ifdef CREATE_FUN
CREATE_FUN() {
#elif !defined(COMPAT_FLAG) || defined(ZEBEDEE)
void create() {
#else
void reset(arg) {
    if (arg)
	return;
#endif
    my_create();
}

status check_system_field(mapping data, string field) {
    return data[SYSTEM] && member(data[SYSTEM], field) != -1;
}

mapping add_system_field(mapping data, string field) {
    if (data[SYSTEM]) {
	if (!check_system_field(data, field))
	    data[SYSTEM] += ({ field });
    }
    else
	data[SYSTEM] = ({ field });
    return data;
}

/*
 * Read the INETD_HOSTS file and set the "hosts" mapping from it.
 * Retain existing HOST_STATUS fields.
 */
void set_host_list() {
    mixed data;

    if (data = read_file(HOST_FILE)) {
	mapping old_hosts;
	int i, stat;
	string *local_cmds;
	string name;

	old_hosts = hosts;
	hosts = ([ ]);
	for(i = sizeof(data = explode(data, "\n")); i--; ) {
	    if (data[i] == "" || data[i][0] == '#')
		continue;
	    if (sizeof(data[i] = explode(data[i], HOSTFILE_DEL)) < 5) {
		log_file(INETD_LOG_FILE, "*Parse error in hosts file: line " +
		(i + 1) + "\n\n");
		continue;
	    }
	    name = lower_case(data[i][HOST_NAME]);
	    if (member(local_cmds = explode(data[i][LOCAL_COMMANDS], HOSTFILE_DEL2), "*") != -1)
		local_cmds = local_cmds - ({ "*" }) + COMMANDS;
	    hosts[name] = ({
		capitalize(data[i][HOST_NAME]),
		data[i][HOST_IP],
		to_int(data[i][HOST_UDP_PORT]),
		local_cmds,
		explode(data[i][HOST_COMMANDS], HOSTFILE_DEL2),
		UNKNOWN
	    });
	    /*
	     * Retain existing host status as long as the IP and
	     * UDP ports are the same.
	     */
	    if (old_hosts[name] &&
	    old_hosts[name][HOST_IP] == hosts[name][HOST_IP] &&
	    old_hosts[name][HOST_UDP_PORT] == hosts[name][HOST_UDP_PORT])
		hosts[name][HOST_STATUS] = old_hosts[name][HOST_STATUS];
	}
    }
    else
	log_file(INETD_LOG_FILE, "*Error in reading host file.\n\n");
}

/*
 * Make a PING request to all muds in the "hosts" mapping to set
 * HOST_STATUS information.
 */
void startup() {
    string *muds;
    int i;

    for(i = sizeof(muds = m_indices(hosts)); i--; )
	send_udp(muds[i], ([ REQUEST: PING ]), 1);
}

/*
 * Remove a buffered packet from the "incoming_packets" mapping.
 */
void remove_incoming(string id) {
    incoming_packets = m_delete(incoming_packets, id);
}

/*
 * Decode a string from a UDP packet.
 * Returns:   The actual value of the argument (either int or string)
 */
mixed decode(string arg) {
    if (arg[0] == '$')
	return arg[1..];
#ifdef RESTRICTED_CASTS
    if (to_string(to_int(arg)) == arg)
	return to_int(arg);
#else
    if ((string)((int)arg) == arg)
	return (int)arg;
#endif
    return arg;
}

/*
 * Decode a UDP packet.
 * Arguments: UDP packet as a string.
 * Returns:   The decoded information as a mapping, 1 for succes but no
 *            output (buffered packet), or 0 on error.
 */
mixed decode_packet(string packet, string delimiter) {
    string *data;
    mapping ret;
    string info, tmp;
    mixed class;
    int i, id, n;

    /* If this packet has been split, handle buffering. */
    if (packet[0..strlen(PACKET)] == PACKET + ":") {
	if (sscanf(packet, PACKET + ":%s:%d:%d/%d" + delimiter + "%s",
	class, id, i, n, tmp) != 5)
	    return 0;
	class = lower_case(class) + ":" + id;
	if (pointerp(incoming_packets[class])) {
	    incoming_packets[class][i-1] = tmp;
	    if (member(incoming_packets[class], 0) == -1) {
		ret =
		decode_packet(implode(incoming_packets[class], ""), delimiter);
		remove_incoming(class);
		return ret;
	    }
	} else {
	    incoming_packets[class] = allocate(n);
	    incoming_packets[class][i-1] = tmp;
	    /* If no timeout is running then start one. */
	    if (!pending_data[class]) {
		call_out("remove_incoming",
		REPLY_TIME_OUT + REPLY_TIME_OUT * RETRIES, class);
	    } else {
		DEBUG("\n*** Buffered packet Timeout already running! ***\n");
	    }
	}
	return 1;
    }
    ret = ([ ]);
    for(i = 0, n = sizeof(data = explode(packet, delimiter)); i < n; i++) {
	/* DATA fields can be denoted by a preceeding blank field. */
	if (data[i] == "") {
	    tmp = DATA;
	    /* Test for illegal packet length (no DATA) */
	    if (++i >= n)
		return 0;
	    info = data[i];
	}
	else if (sscanf(data[i], "%s:%s", tmp, info) != 2)
	    return 0;
	switch((string)(class = decode(tmp))) {
	    case DATA:
		return ret + ([ DATA: decode(implode(
		    ({ info }) + data[i+1..], delimiter)) ]);
	    case SYSTEM:
		ret[class] = explode(info, ":");
		continue;
	    default:
		ret[class] = decode(info);
		continue;
	}
    }
    return ret;
}

/* Check wether a UDP packet was valid.
 * Logs are made and "host" information is updated as appropriate.
 * Arguments: Decoded UDP packet (mapping)
 * Returns:   0 for valid packets, an error string otherwise.
 */
string valid_request(mapping data) {
    mixed host_data;
    string *muds;
    string req;
    int i;

    if (!data[NAME] || !data[UDP_PORT])
	return DATE + ": Illegal packet.\n";
    if (host_data = hosts[lower_case(data[NAME])]) {
	if (data[HOST] != host_data[HOST_IP]) {
	    if (data[NAME] == LOCAL_NAME)
		return DATE + ": *** FAKE MUD ***\n";
	    log_file(INETD_LOG_FILE, DATE + ": Host change:\n" +
	    host_data[HOST_NAME] + ": " + host_data[HOST_IP] + " -> " +
	    data[HOST] + "\n\n");
	    host_data[HOST_IP] = data[HOST];
	}
	if (data[UDP_PORT] != host_data[HOST_UDP_PORT]) {
	    if (data[NAME] == LOCAL_NAME)
		return DATE + ": *** FAKE MUD ***\n";
	    log_file(INETD_LOG_FILE, DATE + ": Port change:\n" +
		host_data[HOST_NAME] + " (" + host_data[HOST_IP] + "): " +
		host_data[HOST_UDP_PORT] + " -> " + data[UDP_PORT] + "\n\n");
	    host_data[HOST_UDP_PORT] = data[UDP_PORT];
	}
    } else {
	if (lower_case(data[NAME]) == lower_case(LOCAL_NAME))
	    return DATE + ": *** FAKE MUD ***\n";
	for(i = sizeof(muds = m_indices(hosts)); i--; ) {
	    host_data = hosts[muds[i]];
	    if (data[HOST] == host_data[HOST_IP] &&
	    data[UDP_PORT] == host_data[HOST_UDP_PORT]) {
		log_file(INETD_LOG_FILE, DATE + ": Name change:\n" +
		    host_data[HOST_NAME] + " (" + host_data[HOST_IP] +
		    ") -> " + data[NAME] + "\n\n");
		host_data[HOST_NAME] = data[NAME];
		hosts[lower_case(data[NAME])] = host_data;
		hosts = m_delete(hosts, muds[i]);
		i = -2;
		break;
	    }
	}
	if (i != -2) {
	    host_data = hosts[lower_case(data[NAME])] = ({
		data[NAME],
		data[HOST],
		data[UDP_PORT],
		COMMANDS,
		({ "*" }),
		UP
	    });
	    log_file(INETD_LOG_FILE, DATE + ": New mud.\n" + data[NAME] + ":" +
	    data[HOST] + ":" + data[UDP_PORT] + "\n\n");
	}
    }
    if (!(req = data[REQUEST]))
	return DATE + ": System message.\n";
    if (req != PING &&
	req != QUERY &&
	req != REPLY &&
	member(host_data[LOCAL_COMMANDS], req) == -1)
    {
	/* This should probably send a system message too. */
	send_udp(host_data[HOST_NAME], ([
	    REQUEST: REPLY,
	    RECIPIENT: data[SENDER],
	    ID: data[ID],
	    DATA: "Invalid request @" + LOCAL_NAME + ": " +
		capitalize(data[REQUEST]) + "\n"
	]) );
	return DATE + ": Invalid request.\n";
    }
    return 0;
}

/*
 * Incoming UDP packets are sent to this function to be interpretted.
 * The packet is decoded, checked for validity, HOST_STATUS is updated
 * and the appropriate udp module called.
 * Arguments: Senders IP address (string)
 *            UDP packet (string)
 */
void receive_udp(string sender, string packet) {
    mapping data;
    string req, err, id;

/* log_file(INETD_LOG_FILE,
 *	    sprintf("got sender %O packet %O\n", sender, packet));
 */
#ifdef UDP_MASTER
    if (!previous_object() ||
    object_name(previous_object()) != UDP_MASTER) {
	log_file(INETD_LOG_FILE, DATE + ": Illegal call of receive_udp() by " +
	object_name(previous_object()) + "\n\n");
	return;
    }
#endif

    if (
#ifdef DELIMITER_COMPAT
	   (    !mappingp(data = decode_packet(packet, NEW_DELIMITER))
	     ||    (data[HOST] = sender)
                && (err = valid_request(data))
           )
        && (    !mappingp(data = decode_packet(packet, OLD_DELIMITER))
             ||    (data[HOST] = sender)
                && (err = valid_request(data))
           )
#else
	!(data = decode_packet(packet, DELIMITER))
#endif
    ) {
	if (!data)
#ifdef RECEIVE_UDP_COMPAT
	    RECEIVE_UDP_COMPAT(sender, packet);
#else
	    log_file(INETD_LOG_FILE, DATE + ": Received invalid packet.\nSender: " +
	    sender + "\nPacket:\n" + packet + "\n\n");
#endif
	return;
    }

#ifdef DELIMITER_COMPAT
    if (!mappingp(data))
	return;
    if (err)
#else
    data[HOST] = sender;
    if (err = valid_request(data))
#endif
    {
	log_file(INETD_LOG_FILE, err + "Sender: " + sender + "\nPacket:\n" +
	packet + "\n\n");
	return;
    }
    hosts[lower_case(data[NAME])][HOST_STATUS] = UP;
    if ((req = data[REQUEST]) == REPLY) {
    	mapping pending;

	/* If we can't find the reply in the pending list then bin it. */
	if (!(pending = pending_data[lower_case(data[NAME]) + ":" + data[ID]]))
	    return;
	/* Set data[REQUEST] correctly, but still send to (req = REPLY) */
	data[REQUEST] = pending[REQUEST];
#ifdef INETD_DIAGNOSTICS
	data[PACKET_LOSS] = pending[PACKET_LOSS];
	data[RESPONSE_TIME] = time() - pending[RESPONSE_TIME] + 1;
#endif
#if 0
/* channel replies may not include a recipient, and shouldn't have one set */
	/* Restore the RECIPIENT in replies if none given and it is known. */
	if (!data[RECIPIENT] && pending[SENDER])
	    data[RECIPIENT] = pending[SENDER];
#endif
	pending_data =
	m_delete(pending_data, lower_case(data[NAME]) + ":" + data[ID]);
    }
    else if (data[ID]) {
	if (member(received_ids, id = (lower_case(data[NAME]) + ":" + data[ID])) == -1)
	{
	    received_ids += ({ id });
	    call_out("remove_received_id",
	    REPLY_TIME_OUT + REPLY_TIME_OUT * (RETRIES + 1), id);
	}
	else
	    add_system_field(data, REPEAT);
    }
    if (err = catch(
	call_other(UDP_CMD_DIR + req, "udp_" + req, copy(data))))
    {
	send_udp(data[NAME], ([
	    REQUEST: REPLY,
	    RECIPIENT: data[SENDER],
	    ID: data[ID],
	    DATA: capitalize(req)+ " request failed @" + LOCAL_NAME + ".\n"
	]) );
	log_file(INETD_LOG_FILE, DATE + ": " + data[REQUEST] + " from " +
	data[NAME] + " failed.\n" + err + packet + "\n\n");
    }
}

int do_match(string mudname, string match_str) {
    return mudname[0..strlen(match_str)-1] == match_str;
}

#ifdef NO_CLOSURES

status greater(mixed a, mixed b) {
    return a > b;
}

string *expand_mud_name(string name) {
    return sort_array(
	filter(m_indices(hosts), "do_match", this_object(), name),
	"greater",
	this_object()
    );
}

#else

string *expand_mud_name(string name) {
    return sort_array(
	filter(m_indices(hosts), #'do_match, name),
	#'>
    );
}

#endif

string encode(mixed arg) {
    if (objectp(arg))
	return object_name(arg);
    if (stringp(arg) && (arg[0] == '$' ||
#ifdef RESTRICTED_CASTS
    to_string(to_int(arg)) == (string)arg))
#else
    (string)to_int(arg) == (string)arg))
#endif
	return "$" + arg;
    return to_string(arg);
}

string encode_packet(mapping data) {
    int i;
    mixed indices;
    string header, body, t1, t2;
    string *ret;
    status data_flag;

    for(ret = ({ }), i = sizeof(indices = m_indices(data)); i--; ) {
	if (indices[i] == DATA) {
	    data_flag = 1;
	    continue;
	}
	header = encode(indices[i]);
	body = encode(data[indices[i]]);
	if (sscanf(header, "%s:%s", t1, t2) ||
	    sscanf(header + body, "%s" + DELIMITER + "%s", t1, t2)
	)
	    return 0;

	ret += ({ header + ":" + body });
    }
    if (data_flag)
#ifdef USE_OLD_DATA_FORMAT
	ret += ({ DATA + ":" + encode(data[DATA]) });
#else
	ret += ({ "", encode(data[DATA]) });
#endif
    return implode(ret, DELIMITER);
}

string *explode_packet(string packet, int len) {
    if (strlen(packet) <= len)
	return ({ packet });
    return ({ packet[0..len-1] }) +
    explode_packet(
    packet[len..]
    , len);
}

varargs string send_udp(string mudname, mapping data, int expect_reply) {
    mixed host_data;
    string *packet_arr;
    string packet;
    int i;

    mudname = lower_case(mudname);
    if (!(host_data = hosts[mudname])) {
	string *names;

	if (sizeof(names = expand_mud_name(mudname)) == 1)
	    host_data = hosts[mudname = names[0]];
	else
#ifdef SEND_UDP_COMPAT
	    return (string)SEND_UDP_COMPAT(mudname, data, expect_reply);
#else
	    return "Unknown or ambiguous mudname: " + capitalize(mudname) + "\n";
#endif
    }
    if (data[REQUEST] != PING &&
    data[REQUEST] != QUERY &&
    data[REQUEST] != REPLY &&
    member(host_data[HOST_COMMANDS], "*") == -1 &&
    member(host_data[HOST_COMMANDS], data[REQUEST]) == -1)
	return capitalize(data[REQUEST]) + ": Command unavailable @" +
	host_data[HOST_NAME] + "\n";
    data[NAME] = LOCAL_NAME;
    data[UDP_PORT] = LOCAL_UDP_PORT;
    if (expect_reply) {
	/* Don't use zero. */
	data[ID] = ++packet_id;
	/* Don't need copy() as we are changing the mapping size. */
	pending_data[mudname + ":" + packet_id] =
#ifdef INETD_DIAGNOSTICS
	data + ([ NAME: host_data[HOST_NAME], RESPONSE_TIME: time() ]);
#else
	data + ([ NAME: host_data[HOST_NAME] ]);
#endif
    }
    if (!(packet = encode_packet(data))) {
	if (expect_reply)
	    pending_data = m_delete(pending_data, mudname + ":" + packet_id);
	log_file(INETD_LOG_FILE, DATE + ": Illegal packet sent by " +
	object_name(previous_object()) + "\n\n");
	return "inetd: Illegal packet.\n";
    }
    if (expect_reply)
	call_out("reply_time_out", REPLY_TIME_OUT, mudname + ":" + packet_id);
    if (strlen(packet) <= MAX_PACKET_LEN)
	packet_arr = ({ packet });
    else {
	string header;
	int max;

	/* Be careful with the ID.  data[ID] could have been set up by RETRY */
	header =
	    PACKET + ":" + lower_case(LOCAL_NAME) + ":" +
	    ((expect_reply || data[REQUEST] != REPLY)&& data[ID] ?
	    data[ID] : ++packet_id) + ":";
	/* Allow 8 extra chars: 3 digits + "/" + 3 digits + DELIMITER */
	packet_arr = explode_packet(packet,
	    MAX_PACKET_LEN - (strlen(header) + 8));
	for(i = max = sizeof(packet_arr); i--; )
	    packet_arr[i] =
	    header + (i+1) + "/" + max + DELIMITER + packet_arr[i];
    }
    for(i = sizeof(packet_arr); i--; ) {
	if (!send_imp(
	host_data[HOST_IP], host_data[HOST_UDP_PORT], packet_arr[i]))
	    return "inetd: Error in sending packet.\n";
    }
    return 0;
}

void reply_time_out(string id) {
    mapping data;

    if (data = pending_data[id]) {
	object ob;

#ifdef INETD_DIAGNOSTICS
	data[PACKET_LOSS]++;
#endif
	if (data[RETRY] < RETRIES) {
	    mapping send;

	    data[RETRY]++;
	    /* We must use a copy so the NAME field in pending_data[id]
	     * isn't corrupted by send_udp(). */
	    send = copy(data);
	    send = m_delete(send, RETRY);
#ifdef INETD_DIAGNOSTICS
	    send = m_delete(send, PACKET_LOSS);
	    send = m_delete(send, RESPONSE_TIME);
#endif
	    call_out("reply_time_out", REPLY_TIME_OUT, id);
	    send_udp(data[NAME], send);
	    return;
	}
	data = m_delete(data, RETRY);
#ifdef INETD_DIAGNOSTICS
	data = m_delete(data, RESPONSE_TIME);
#endif
	catch(call_other(UDP_CMD_DIR + REPLY, "udp_" + REPLY,
	add_system_field(data, TIME_OUT)));
	/* It's just possible this was removed from the host list. */
	if (hosts[lower_case(data[NAME])])
	    hosts[lower_case(data[NAME])][HOST_STATUS] = DOWN;
	remove_incoming(lower_case(data[NAME]) + ":" + id);
    }
    pending_data = m_delete(pending_data, id);
}

void remove_received_id(string id) {
    received_ids -= ({ id });
}

varargs mixed query(string what, mixed extra1, mixed extra2) {
    mixed data;

    switch(what) {
	case "commands":
	    return COMMANDS;
	case "hosts":
	    return copy(hosts);
	case "pending":
	    return copy(pending_data);
	case "incoming":
	    return copy(incoming_packets);
	case "received":
	    return ({ }) + received_ids;
	/* args: "valid_request", request, mudname */
	case "valid_request":
	    if (data = hosts[extra2])
		return member(data[HOST_COMMANDS], "*") != -1 ||
			member(data[HOST_COMMANDS], extra1) != -1;
	    return 0;
    }
}
